---
title: SAIF.Business.Tasks business rules
summary: Enumerated business rules with per-rule trigger, enforcement, and outcomes.
status: draft
owner: SAIF Documentation
last_reviewed: 2026-08-21
library: SAIF.Business.Tasks
reference_type: embedded
---

# SAIF.Business.Tasks business rules

This page captures business rules enforced by this library.

## BR-001 A task must have a Task Category Code

| Field | Value |
| --- | --- |
| **Statement** | A task must have a Task Category Code. |
| **Trigger** | Rule evaluation on `CategoryCode` (registered twice) |
| **Enforcement** | `clsTask.AddBusinessRules` → `CommonBusinessRules.TextRequired` |
| **Outcome** | Error "Error: A Task Category Code is required."; save blocked* |
| **Tables** | [TASK](database-tables.md#task) |
| **SQL** | [SQL-001](sql/SQL-001-insert-task.md) |
| **Source** | SAIF.Business.Tasks/clsTask.vb |

---

## BR-002 A task must have a Source System Code

| Field | Value |
| --- | --- |
| **Statement** | A task must have a Source System Code. |
| **Trigger** | Rule evaluation on `SourceCode` |
| **Enforcement** | `clsTask.AddBusinessRules` → `CommonBusinessRules.TextRequired` |
| **Outcome** | Error "Error: A Source System Code is required." |
| **Source** | SAIF.Business.Tasks/clsTask.vb |

---

## BR-003 A task must have a Priority Code

| Field | Value |
| --- | --- |
| **Statement** | A task must have a Priority Code. |
| **Trigger** | Rule evaluation on `PriorityCode` |
| **Enforcement** | `clsTask.AddBusinessRules` → `CommonBusinessRules.TextRequired` |
| **Outcome** | Error "Error: A Priority Code is required." |
| **Source** | SAIF.Business.Tasks/clsTask.vb |

---

## BR-004 A task must be assigned to a MailID

| Field | Value |
| --- | --- |
| **Statement** | A task must be assigned to a MailID. |
| **Trigger** | `MailID` setter calls `CheckRules("TaskUser")`; rule registered on `MailID` |
| **Enforcement** | `clsTask.AddBusinessRules` / `MailID` setter |
| **Outcome** | Error "Error: A MailID is required." |
| **Source** | SAIF.Business.Tasks/clsTask.vb |

---

## BR-005 The Task Source Code Task Category Code combination must exist

| Field | Value |
| --- | --- |
| **Statement** | The Task Source Code / Task Category Code combination must exist in `TASK_CATEGORY_CODE` (rule `TASK1`). |
| **Trigger** | Rule evaluation on `SourceCode` |
| **Enforcement** | `clsTaskBusinessRules.ValidSourceCategory` — `SELECT COUNT(*) FROM Task_Category_Code WHERE TASK_SOURCE_CODE = :Source AND CODE = :Category` |
| **Outcome** | Count > 0 → pass; otherwise descendant [BR-026](#br-026-descendant-task-types-may-override-common-rule-outcomes) decides; failing yields "Error: Invalid Task Source and Category Combination" at `RuleCheckLevel.LevelThree` |
| **Tables** | [TASK_CATEGORY_CODE](database-tables.md#task_category_code) |
| **SQL** | [SQL-005](sql/SQL-005-valid-source-category-check.md) |
| **Source** | SAIF.Business.Tasks/clsTaskBusinessRules.vb |

---

## BR-006 A task must carry a current or future date

| Field | Value |
| --- | --- |
| **Statement** | A task must carry a current or future date (rule `TASK2`). |
| **Trigger** | Rule evaluation on `TaskDate` |
| **Enforcement** | `clsTaskBusinessRules.CheckDate` → `CommonBusinessRules.IsTodayOrFutureDate` |
| **Outcome** | Checked only when the object is dirty, not deleted, and the date was modified or is `Date.MinValue`; failure → "Error: A Task must have a current or future date" unless overridden by the descendant |
| **Source** | SAIF.Business.Tasks/clsTaskBusinessRules.vb |

---

## BR-007 Batch-created tasks bypass the currentfuture date rule

| Field | Value |
| --- | --- |
| **Statement** | Batch-created tasks bypass the current/future date rule. |
| **Trigger** | `BatchRules = True` |
| **Enforcement** | `clsTaskBusinessRules.CheckDate` early return |
| **Outcome** | Rule passes unconditionally |
| **Source** | SAIF.Business.Tasks/clsTaskBusinessRules.vb |

---

## BR-008 Automatic due-date shifting must be surfaced as a warning

| Field | Value |
| --- | --- |
| **Statement** | Automatic due-date shifting must be surfaced to the user as a warning (rule `TASK4`). |
| **Trigger** | `TaskDateModified` is true after `AssignTaskDate` |
| **Enforcement** | `clsTaskBusinessRules.CheckDateModified` |
| **Outcome** | Warning "Due Date has been changed to the next work day." or "...prior work day." per `TaskDateModifier`; `RuleType.Warning` — does not block save* |
| **Source** | SAIF.Business.Tasks/clsTaskBusinessRules.vb |

---

## BR-009 A non-working-day due date is moved to a working day

| Field | Value |
| --- | --- |
| **Statement** | A non-working-day due date is moved to the prior working day by default (or the next working day when configured). |
| **Trigger** | Any `TaskDate` assignment with a real date |
| **Enforcement** | `clsTask.AssignTaskDate` |
| **Outcome** | Date normalized to a working day; `TaskDateModified` set |
| **Source** | SAIF.Business.Tasks/clsTask.vb |

---

## BR-010 Task insert must affect exactly one row

| Field | Value |
| --- | --- |
| **Statement** | Task insert must affect exactly one row. |
| **Trigger** | `AfterInsert` |
| **Enforcement** | `clsTask.AfterInsert` |
| **Outcome** | `RowsAffected <> 1` → error message `InsertError` = "Task Insert Failed"; otherwise `MarkOld()` |
| **Tables** | [TASK](database-tables.md#task) |
| **SQL** | [SQL-001](sql/SQL-001-insert-task.md) |
| **Source** | SAIF.Business.Tasks/clsTask.vb |

---

## BR-011 Task update must affect exactly one row

| Field | Value |
| --- | --- |
| **Statement** | Task update must affect exactly one row, else a concurrency conflict is reported. |
| **Trigger** | `AfterUpdate` |
| **Enforcement** | `clsTask.AfterUpdate` |
| **Outcome** | `RowsAffected <> 1` → `UpdateError` = `clsConstants.Messages.DBContentionError`; otherwise `UpdateSeqNum` incremented and `MarkOld()` |
| **Tables** | [TASK](database-tables.md#task) |
| **SQL** | [SQL-013](sql/SQL-013-update-task.md) |
| **Source** | SAIF.Business.Tasks/clsTask.vb |

---

## BR-012 Task delete must affect exactly one row

| Field | Value |
| --- | --- |
| **Statement** | Task delete must affect exactly one row, else a concurrency conflict is reported. |
| **Trigger** | `AfterDelete` |
| **Enforcement** | `clsTask.AfterDelete` |
| **Outcome** | `RowsAffected <> 1` → `DeleteError` = `clsConstants.Messages.DBContentionError` |
| **Tables** | [TASK](database-tables.md#task) |
| **SQL** | [SQL-014](sql/SQL-014-delete-task.md) |
| **Source** | SAIF.Business.Tasks/clsTask.vb |

---

## BR-013 Optimistic concurrency via update sequence number

| Field | Value |
| --- | --- |
| **Statement** | Optimistic concurrency is enforced by `UPDATE_SEQ_NUM`, which increments on each successful update. |
| **Trigger** | Update / delete |
| **Enforcement** | `clsTask.BeforeUpdate`, `BeforeDelete` (parameter `UpdateSeqNum`), `AfterUpdate` |
| **Outcome** | Stale sequence number causes zero rows affected → [BR-011](#br-011-task-update-must-affect-exactly-one-row) / [BR-012](#br-012-task-delete-must-affect-exactly-one-row) |
| **Tables** | [TASK](database-tables.md#task) |
| **SQL** | [SQL-013](sql/SQL-013-update-task.md), [SQL-014](sql/SQL-014-delete-task.md) |
| **Source** | SAIF.Business.Tasks/clsTask.vb |

---

## BR-014 Creating user defaults to the data-portal user identity

| Field | Value |
| --- | --- |
| **Statement** | The creating user defaults to the data-portal user identity when the caller supplies none. |
| **Trigger** | `Create()` |
| **Enforcement** | `clsTask.Create` |
| **Outcome** | `m_CreateUser = MyCriteria.DataPortalInfo.UserID`; `UpdateUser` mirrors it |
| **Source** | SAIF.Business.Tasks/clsTask.vb |

---

## BR-015 New tasks start not-completed and without a notepad entry

| Field | Value |
| --- | --- |
| **Statement** | New tasks start not-completed and without a notepad entry. |
| **Trigger** | `Create()` |
| **Enforcement** | `clsTask.Create` |
| **Outcome** | `DeleteFlag = "N"`, `NotepadFlag = "N"` |
| **Source** | SAIF.Business.Tasks/clsTask.vb |

---

## BR-016 MailIDs are stored and compared trimmed and upper-cased

| Field | Value |
| --- | --- |
| **Statement** | MailIDs are stored and compared trimmed and upper-cased. |
| **Trigger** | `MailID` setter / `Criteria.MailID` |
| **Enforcement** | `clsTask.MailID`, `clsTaskCollection.Criteria.MailID` |
| **Outcome** | Normalized value persisted and used in the `TASK_USER` predicate |
| **Tables** | [TASK](database-tables.md#task) |
| **SQL** | [SQL-001](sql/SQL-001-insert-task.md), [SQL-008](sql/SQL-008-retrieve-filtered-task-list.md), [SQL-013](sql/SQL-013-update-task.md) |
| **Source** | SAIF.Business.Tasks/clsTask.vb |

---

## BR-017 Completed tasks are excluded from searches unless ShowCompleted is set

| Field | Value |
| --- | --- |
| **Statement** | Completed (deleted) tasks are excluded from searches unless `ShowCompleted` is set. |
| **Trigger** | Collection retrieve |
| **Enforcement** | `clsTaskCollection.Criteria.FormatOtherParameters` |
| **Outcome** | Appends `AND DELETE_FLAG = 'N'` when `ShowCompleted` is false |
| **Tables** | [TASK](database-tables.md#task) |
| **SQL** | [SQL-008](sql/SQL-008-retrieve-filtered-task-list.md) |
| **Source** | SAIF.Business.Tasks/clsTaskCollection.vb |

---

## BR-018 A priority-level filter selects that priority and all higher ones

| Field | Value |
| --- | --- |
| **Statement** | A priority-level filter selects that priority and all higher ones. |
| **Trigger** | `PriorityLevel` set to Low/Medium/High/Urgent |
| **Enforcement** | `clsTaskCollection.Criteria.FormatPriorityParameter` |
| **Outcome** | Emits `TASK_PRIORITY_CODE IN (...)` for the level and above; `Informational` means no priority filter; an explicit `TaskPriority` code takes precedence |
| **Tables** | [TASK](database-tables.md#task) |
| **SQL** | [SQL-008](sql/SQL-008-retrieve-filtered-task-list.md) |
| **Source** | SAIF.Business.Tasks/clsTaskCollection.vb |

---

## BR-019 Filter values accept single values or lists

| Field | Value |
| --- | --- |
| **Statement** | Claim, policy, category, source, and action filters accept single values or lists (single `=` vs `IN`); zero/blank entries are rejected at add time. |
| **Trigger** | `AddClaimID`, `AddPolicyNumber`, `AddTaskCategory`, `AddTaskActionCode`, `AddTaskSourceSystem` |
| **Enforcement** | `clsTaskCollection.Criteria` |
| **Outcome** | Values ≤ 0 or blank are not added; one entry → equality predicate, many → `IN` list |
| **Tables** | [TASK](database-tables.md#task) |
| **SQL** | [SQL-008](sql/SQL-008-retrieve-filtered-task-list.md) |
| **Source** | SAIF.Business.Tasks/clsTaskCollection.vb |

---

## BR-020 Date-range filtering compares the truncated task date

| Field | Value |
| --- | --- |
| **Statement** | Date-range filtering compares the truncated `TASK_DATE` and is only applied when the bound differs from `Date.MinValue`/`Date.MaxValue`. |
| **Trigger** | Collection retrieve |
| **Enforcement** | `FormatDateParameters` |
| **Outcome** | `AND trunc(TASK_DATE) >= :BeginDate` / `<= :EndDate` |
| **Tables** | [TASK](database-tables.md#task) |
| **SQL** | [SQL-008](sql/SQL-008-retrieve-filtered-task-list.md) |
| **Source** | SAIF.Business.Tasks/clsTaskCollection.vb |

---

## BR-021 Result sets may be capped to N most-relevant rows

| Field | Value |
| --- | --- |
| **Statement** | Result sets may be capped to N most-relevant rows using an inner sort, then re-sorted for display. |
| **Trigger** | `RowNumLimit > 0` |
| **Enforcement** | `FormatRowLimitBegin` / `FormatRowLimitEnd` |
| **Outcome** | Wraps the query in `select * from ( … Order by <InnerSort> ) where rownum <= N Order by <OuterSort>` |
| **Tables** | [TASK](database-tables.md#task) |
| **SQL** | [SQL-008](sql/SQL-008-retrieve-filtered-task-list.md) |
| **Source** | SAIF.Business.Tasks/clsTaskCollection.vb |

---

## BR-022 Retrieval of a task with no concrete task class is not permitted

| Field | Value |
| --- | --- |
| **Statement** | Retrieval of a task whose source system has no concrete task class is not permitted. |
| **Trigger** | `AfterRetrieve` row materialization |
| **Enforcement** | `clsTaskCollection.AfterRetrieve` |
| **Outcome** | Throws `ApplicationException("<source> Tasks are not yet supported!")` |
| **Tables** | [TASK](database-tables.md#task) |
| **SQL** | [SQL-008](sql/SQL-008-retrieve-filtered-task-list.md) |
| **Source** | SAIF.Business.Tasks/clsTaskCollection.vb |

---

## BR-023 Code tables are read-only reference data

| Field | Value |
| --- | --- |
| **Statement** | Code tables are read-only reference data. |
| **Trigger** | Any attempt to use create/insert/update/delete SQL on a code table criteria |
| **Enforcement** | `Criteria` classes in all `clsTask*Code[Collection]` types |
| **Outcome** | Throws `ApplicationException(clsConstants.Messages.InvalidSQLProperty)` |
| **Tables** | [TASK_ACTION_CODE](database-tables.md#task_action_code), [TASK_CATEGORY_CODE](database-tables.md#task_category_code), [TASK_PRIORITY_CODE](database-tables.md#task_priority_code), [TASK_SOURCE_CODE](database-tables.md#task_source_code) |
| **Source** | SAIF.Business.Tasks/clsTaskActionCode.vb |

---

## BR-024 A category-code collection is valid only if at least one row was found

| Field | Value |
| --- | --- |
| **Statement** | A category-code collection is valid only if at least one row was returned for the requested source system. |
| **Trigger** | `AfterRetrieve` |
| **Enforcement** | `clsTaskCategoryCodeCollection.IsValid` / `m_DataFound` |
| **Outcome** | `IsValid = False` when the source system has no categories |
| **Tables** | [TASK_CATEGORY_CODE](database-tables.md#task_category_code) |
| **SQL** | [SQL-007](sql/SQL-007-select-task-category-code-list.md) |
| **Source** | SAIF.Business.Tasks/clsTaskCategoryCodeCollection.vb |

---

## BR-025 Unrecognized codes map to a safe default

| Field | Value |
| --- | --- |
| **Statement** | An unrecognized priority code maps to `Informational`; an unrecognized source code maps to `NotAssigned`. |
| **Trigger** | Code↔enum translation |
| **Enforcement** | `clsTaskPriorityCodeCollection.Priority`, `clsTaskSourceCodeCollection.TaskSource` |
| **Outcome** | Safe default returned instead of an exception |
| **Source** | SAIF.Business.Tasks/clsTaskPriorityCodeCollection.vb |

---

## BR-026 Descendant task types may override common rule outcomes

| Field | Value |
| --- | --- |
| **Statement** | Descendant task types may override the outcome of common rules TASK1, TASK2, and TASK4. |
| **Trigger** | Rule failure in `clsTaskBusinessRules` |
| **Enforcement** | `clsTask.OverrideRules` (overridable, no-op by default) |
| **Outcome** | Descendant can convert a failure into a pass or adjust the message |
| **Source** | SAIF.Business.Tasks/clsTask.vb |

---

## BR-027 Task text is never null

| Field | Value |
| --- | --- |
| **Statement** | Task text is never null. |
| **Trigger** | `Text` setter |
| **Enforcement** | `clsTask.Text` |
| **Outcome** | `Nothing` is coerced to `String.Empty` before comparison/assignment |
| **Source** | SAIF.Business.Tasks/clsTask.vb |

---

## Stored procedure business rules

Rules sourced from Oracle stored procedures referenced by this library.

| Rule ID | Rule | Source | Link |
| --- | --- | --- | --- |
| PR-BT-001 | A zero claim identifier is not a real claim and must not be stored, because it breaks referential integrity on `TASK`. | `pk82036_task.task_insert` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-002 | A zero policy number is not a real policy and must not be stored. | `pk82036_task.task_insert` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-003 | A newly created task is always active; the caller cannot insert a task in a deleted state. | `pk82036_task.task_insert` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-004 | Task identity and audit stamps are assigned by the database, not the caller. | `pk82036_task.task_insert` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-005 | The caller must be able to detect a failed insert. | `pk82036_task.task_insert` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-006 | A task is retrieved by its surrogate key only; no other predicate (including `delete_flag`) restricts the fetch. | `pk82036_task.task_select` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-007 | An update must not overwrite a concurrent modification made by another user (optimistic concurrency). | `pk82036_task.task_update` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-008 | Every successful update advances the row version and re-stamps the audit trail. | `pk82036_task.task_update` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-009 | The zero-to-null normalisation for claim and policy applies on update as well as insert. | `pk82036_task.task_update` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-010 | Task source and creator are immutable. | `pk82036_task.task_update` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-011 | Task deletion is logical, not physical — task history is retained. | `pk82036_task.task_delete` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-012 | A delete must not clobber a concurrent modification (same optimistic-concurrency contract as update). | `pk82036_task.task_delete` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-013 | A logical delete is itself an auditable change and advances the row version. | `pk82036_task.task_delete` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-014 | Only tasks already logically deleted are eligible for physical removal. | `PK82036_Task.Task_Purge` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-015 | Purging is scoped to a single source/category combination — a caller cannot purge across the whole `TASK` table in one call. | `PK82036_Task.Task_Purge` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-016 | A retention window is enforced by whole days, not by timestamp, so a task is retained for at least `inkeepdays` full days after its last update. | `PK82036_Task.Task_Purge` | [Details](procedures/pk82036-task.md#business-rules) |
| PR-BT-017 | The caller must be told how many rows were physically removed. | `PK82036_Task.Task_Purge` | [Details](procedures/pk82036-task.md#business-rules) |

## PR-BT-001 Zero claim identifier is stored as null

| Field | Value |
| --- | --- |
| **Statement** | A zero claim identifier is not a real claim and must not be stored, because it breaks referential integrity on `TASK`. |
| **Condition** | `claimid = 0` |
| **Action** | Store `NULL` in `task.claim_id` via `decode(claimid, 0, null, claimid)`. Documented inline: *"Modified to force 0 claimid and policynum to null. A 0(zero) claimid or policynum does not make sense and is preventing RI on the table."* |
| **Source procedure** | `pk82036_task.task_insert` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-002 Zero policy number is stored as null

| Field | Value |
| --- | --- |
| **Statement** | A zero policy number is not a real policy and must not be stored. |
| **Condition** | `policynum = 0` |
| **Action** | Store `NULL` in `task.policy_num` via `decode(policynum, 0, null, policynum)`. |
| **Source procedure** | `pk82036_task.task_insert` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-003 New tasks are always active

| Field | Value |
| --- | --- |
| **Statement** | A newly created task is always active; the caller cannot insert a task in a deleted state. |
| **Condition** | Always, on insert |
| **Action** | `delete_flag` is hard-coded to `'N'` in the `VALUES` list, ignoring the supplied `deleteflag` argument. |
| **Source procedure** | `pk82036_task.task_insert` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-004 Task identity and audit stamps are database-assigned

| Field | Value |
| --- | --- |
| **Statement** | Task identity and audit stamps are assigned by the database, not the caller. |
| **Condition** | Always, on insert |
| **Action** | `task_seq_num := task_seq.NEXTVAL`; `create_date := SYSDATE`; `update_date := SYSDATE`; `update_seq_num := 0`. |
| **Source procedure** | `pk82036_task.task_insert` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-005 Failed inserts must be detectable

| Field | Value |
| --- | --- |
| **Statement** | The caller must be able to detect a failed insert. |
| **Condition** | Always, on insert |
| **Action** | Returns `SQL%ROWCOUNT`. `clsTask.AfterInsert` raises the error `"Task Insert Failed"` when `RowsAffected <> 1`, otherwise calls `MarkOld()`. |
| **Source procedure** | `pk82036_task.task_insert` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-006 Task retrieval is keyed by surrogate key only

| Field | Value |
| --- | --- |
| **Statement** | A task is retrieved by its surrogate key only; no other predicate (including `delete_flag`) restricts the fetch. |
| **Condition** | `task.task_seq_num = taskseqnum` |
| **Action** | Opens `recordset` over a 25-column projection of `TASK` with camel-cased column aliases matching the `clsTask` property names. Soft-deleted rows (`delete_flag = 'Y'`) **are** returned. |
| **Source procedure** | `pk82036_task.task_select` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-007 Updates enforce optimistic concurrency

| Field | Value |
| --- | --- |
| **Statement** | An update must not overwrite a concurrent modification made by another user (optimistic concurrency). |
| **Condition** | `task_seq_num = taskseqnum AND update_seq_num = updateseqnum` |
| **Action** | If the version token does not match, zero rows update and `SQL%ROWCOUNT = 0` is returned to the caller. |
| **Source procedure** | `pk82036_task.task_update` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-008 Successful updates advance the row version

| Field | Value |
| --- | --- |
| **Statement** | Every successful update advances the row version and re-stamps the audit trail. |
| **Condition** | On matching row |
| **Action** | `update_seq_num := updateseqnum + 1`; `update_date := SYSDATE`; `update_user := updateuser`. |
| **Source procedure** | `pk82036_task.task_update` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-009 Zero-to-null normalisation applies on update

| Field | Value |
| --- | --- |
| **Statement** | The zero-to-null normalisation for claim and policy applies on update as well as insert. |
| **Condition** | `claimid = 0` or `policynum = 0` |
| **Action** | `claim_id := decode(claimid, 0, null, claimid)`; `policy_num := decode(policynum, 0, null, policynum)`. |
| **Source procedure** | `pk82036_task.task_update` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-010 Task source and creator are immutable

| Field | Value |
| --- | --- |
| **Statement** | Task source and creator are immutable. |
| **Condition** | Always |
| **Action** | Neither `task_source_code` nor `create_user` / `create_date` appears in the `SET` list, and no `tasksourcecode` / `createuser` parameter is exposed. |
| **Source procedure** | `pk82036_task.task_update` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-011 Task deletion is logical, not physical

| Field | Value |
| --- | --- |
| **Statement** | Task deletion is logical, not physical — task history is retained. |
| **Condition** | Always, on delete |
| **Action** | Issues `UPDATE task SET delete_flag = 'Y'` rather than `DELETE`. |
| **Source procedure** | `pk82036_task.task_delete` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-012 Deletes enforce the same optimistic-concurrency contract as updates

| Field | Value |
| --- | --- |
| **Statement** | A delete must not clobber a concurrent modification (same optimistic-concurrency contract as update). |
| **Condition** | `task_seq_num = taskseqnum AND update_seq_num = updateseqnum` |
| **Action** | Zero rows affected when stale; `clsTask.AfterDelete` raises `clsConstants.Messages.DBContentionError` under the `"DeleteError"` key when `RowsAffected <> 1`. |
| **Source procedure** | `pk82036_task.task_delete` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-013 Logical deletes advance the row version

| Field | Value |
| --- | --- |
| **Statement** | A logical delete is itself an auditable change and advances the row version. |
| **Condition** | On matching row |
| **Action** | `update_date := SYSDATE`; `update_seq_num := updateseqnum + 1`; `update_user := updateuser`. |
| **Source procedure** | `pk82036_task.task_delete` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-014 Only logically deleted tasks are eligible for physical purge

| Field | Value |
| --- | --- |
| **Statement** | Only tasks already logically deleted are eligible for physical removal. |
| **Condition** | `delete_flag = 'Y'` |
| **Action** | Rows not soft-deleted are never physically purged. |
| **Source procedure** | `PK82036_Task.Task_Purge` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-015 Purge is scoped to a single source/category combination

| Field | Value |
| --- | --- |
| **Statement** | Purging is scoped to a single source/category combination — a caller cannot purge across the whole `TASK` table in one call. |
| **Condition** | `task_source_code = intasksourcecode AND task_category_code = intaskcategorycode` |
| **Action** | Both predicates are mandatory `AND` conditions on the `DELETE`. |
| **Source procedure** | `PK82036_Task.Task_Purge` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-016 Retention window is enforced by whole days

| Field | Value |
| --- | --- |
| **Statement** | A retention window is enforced by whole days, not by timestamp, so a task is retained for at least `inkeepdays` full days after its last update. |
| **Condition** | `TRUNC(update_date) < TRUNC(SYSDATE - TO_NUMBER(inkeepdays))` |
| **Action** | Both sides of the comparison are `TRUNC`-ed to date granularity before comparison. |
| **Source procedure** | `PK82036_Task.Task_Purge` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## PR-BT-017 Purge reports the number of rows removed

| Field | Value |
| --- | --- |
| **Statement** | The caller must be told how many rows were physically removed. |
| **Condition** | Always |
| **Action** | `outdelrows := TO_CHAR(SQL%ROWCOUNT)`; `clsTaskCollection.PurgeTasks` reads it back with `CInt(DataRequest.ParmValue("outdelrows"))` and returns it as `Integer`. |
| **Source procedure** | `PK82036_Task.Task_Purge` |
| **Link** | [Full procedure detail](procedures/pk82036-task.md#business-rules) |

---

## Notes
- The `pk82036_task` PL/SQL package spec and body were retrieved in full via the Legends CLI (`legends oracle query --name pk82036_task`, schema `WCIS_DBA`) — see [Stored procedure business rules](#stored-procedure-business-rules) above and [procedures/pk82036-task.md](procedures/pk82036-task.md) for full parameters, business rules, and sample SQL for `task_insert`, `task_select`, `task_update`, `task_delete`, and `Task_Purge`.

> **Note:** Statements marked with `*` were inferred from code structure, naming conventions, or
> contextual evidence where direct confirmation was not available in source artifacts. Statements
> marked with `**` could not be confirmed; treat as unknown until verified.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.md)
- [Database tables](database-tables.md)
- [SQL catalog](sql/index.md)
- [Procedures](procedures/index.md)
- [API reference](api/index.md)

> **Note:** Statements marked with * were inferred from code structure, naming conventions, or
> contextual evidence where direct confirmation was not available in source artifacts. Statements
> marked with ** could not be confirmed; treat as unknown until verified.

## Sources
- SAIF.Business.Tasks/clsTask.vb
- SAIF.Business.Tasks/clsTaskCollection.vb
- SAIF.Business.Tasks/clsTaskBusinessRules.vb
- SAIF.Business.Tasks/clsTaskActionCode.vb
- SAIF.Business.Tasks/clsTaskCategoryCode.vb
- SAIF.Business.Tasks/clsTaskCategoryCodeCollection.vb
- SAIF.Business.Tasks/clsTaskPriorityCode.vb
- SAIF.Business.Tasks/clsTaskPriorityCodeCollection.vb
- SAIF.Business.Tasks/clsTaskSourceCode.vb
- SAIF.Business.Tasks/clsTaskSourceCodeCollection.vb
